﻿using System;
using System.Diagnostics.Contracts;
using System.Linq;
using StyleCop;
using StyleCop.CSharp;

namespace Jsl.StyleCop.CSharp.RefactoringRules
{
    /// <summary>
    /// Suggests moving variables to the smallest scope they can fit into.
    /// </summary>
    public class MoveVariableDeclarationToUsageLevel : CsRuleBase<SourceAnalyzer>
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="MoveVariableDeclarationToUsageLevel"/> class.
        /// </summary>
        /// <param name="analyzer">The analyzer.</param>
        public MoveVariableDeclarationToUsageLevel(RuleAnalyzer analyzer)
            : base(analyzer)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Analyzes the graph.
        /// </summary>
        /// <param name="element">The element the graph was built from.</param>
        /// <param name="graph">The graph.</param>
        public override void AnalyzeGraph(CsElement element, CodeUnitGraph graph)
        {
            if (this.Analyzer.IsRuleEnabled(this.Document, RuleAnalyzer.MoveVariableDeclarationToUsageLevel))
            {
                this.CheckVariables(element, graph);
            }
        }
        #endregion Public Methods

        #region Private Methods
        private void CheckVariables(CsElement element, CodeUnitGraph graph)
        {
            Contract.Assume(graph.Vertices != null);

            // Get all the variable usages in the graph so we don't have to call GetVariableUsage later.
            // Key = the Literal Expression that is a variable.
            // Value = the variable usage.
            var allVariableUsages =
                (from literalExpression in graph.Vertices.OfType<LiteralExpression>()
                 let variableUsage = literalExpression.GetVariableUsage()
                 where variableUsage != null
                 select variableUsage)
                 .ToDictionary(variableUsage => variableUsage.Source);

            // Get all the unique variables used.
            var uniqueVariables = allVariableUsages
                .Values
                .Select(variableUsage => variableUsage.Variable)
                .Distinct();

            foreach (var variableToCheck in uniqueVariables)
            {
                // Get all the variable usages not including the declaration.
                var thisVariableUsages =
                    from variableUsage in allVariableUsages.Values
                    where !variableUsage.IsDeclaration
                        && object.ReferenceEquals(variableUsage.Variable, variableToCheck)
                    select variableUsage;

                // Ignore any hoisted variables. These are ones used inside delegate or lambda expressions.
                // All bets are off when the variable is hoisted.
                if (!thisVariableUsages.Any(usage => usage.HoistedInside != null))
                {
                    // Get the variable declaration.
                    var variableDeclaration = allVariableUsages
                        .Values
                        .Where(usage => usage.IsDeclaration && object.ReferenceEquals(usage.Variable, variableToCheck))
                        .FirstOrDefault();

                    // If there was one found then process the variable. Parameters won't have declarations so skip them.
                    if ((variableDeclaration != null)
                        && !variableDeclaration.IsCatchVariable
                        && !variableDeclaration.IsForVariable
                        && !variableDeclaration.IsUsingVariable)
                    {
                        // Find the closest common ancestor of all the variable usages for this variable.
                        ICodeUnit closestCommonAncestor = null;
                        foreach (VariableUsageInformation variableUsage in thisVariableUsages)
                        {
                            if (closestCommonAncestor == null)
                            {
                                // This is the first usage to look at so by default, its the best.
                                closestCommonAncestor = variableUsage.SmallestVariableScope();
                            }
                            else
                            {
                                // Find the smallest variable scope...
                                closestCommonAncestor = variableUsage.SmallestVariableScope(closestCommonAncestor);

                                // There should always be a common ancestor since the method/constructor/whatever should
                                // be an ancestor of both.
                                if (closestCommonAncestor == null)
                                {
                                    throw new InvalidOperationException("No common ancestor found.");
                                }
                            }
                        }

                        if ((closestCommonAncestor != null)
                            && !(closestCommonAncestor is DoWhileStatement)
                            && !(closestCommonAncestor is ForStatement)
                            && !(closestCommonAncestor is ForeachStatement)
                            && !(closestCommonAncestor is WhileStatement))
                        {
                            var declarationContainer = variableDeclaration.SmallestVariableScope();

                            // If the declaration isn't at the same level as the usage then flag the issue.
                            if (!object.ReferenceEquals(declarationContainer, closestCommonAncestor))
                            {
                                this.AddViolation(
                                    element,
                                    variableDeclaration.Source.LineNumber,
                                    RuleAnalyzer.MoveVariableDeclarationToUsageLevel,
                                    variableToCheck.Name,
                                    closestCommonAncestor.LineNumber);
                            }
                        }
                    }
                }
            }
        }
        #endregion Private Methods
    }
}